/**
 *Copyright 2013 by dragon.
 *
 *File name: HttpDownLoader.java
 *Author:      dragon
 *Email:       fufulove2012@gmail.com
 *Blog:        http://blog.csdn.net/xidomlove
 *Version:     1.0.0
 *Date:        2013-10-4 下午3:44:25
 *Description: 
 */
package com.dragon.jaxel.xtp;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.Map;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.dragon.log.Logger;

/**
 * @author dragon8
 * 
 */
public class HttpDownloader extends XtpDownloader {

	private String urlString;

	public HttpDownloader() {
		super();
	}

	/**
	 * 
	 */
	public HttpDownloader(String urlString, String savePath) {
		this.urlString = urlString;
		super.savePathString = savePath;
		if (savePath != null) {
			super.savePathString = new File(savePath).getAbsolutePath();
		}
	}

	private HttpURLConnection connect(long rangeBegin) throws IOException {
		return new HttpURLConnection(urlString).connect(rangeBegin);
	}

	@Override
	long getFileLength() {
		try {
			HttpURLConnection connection = connect(0);
			
			long len = -1;
			String contentLenthString = connection
					.getHeaderField("Content-Length");
			
			if (contentLenthString != null) {
				len = Long.valueOf(contentLenthString);
			} else if ((contentLenthString = connection
					.getHeaderField("Content-Range")) != null) {
				len = Long.valueOf(contentLenthString
						.substring(contentLenthString.lastIndexOf('/') + 1));
			} else {
				len = connection.getContentLength();
			}
			
			Logger.getDefaultLogger().info(
					"Http response code: " + connection.getResponseCode());
			
			if (connection.getResponseCode() == 206) {
				isSupportRange = true;
			}
			
			if (super.savePathString == null
					|| new File(savePathString).isDirectory()) {
				getContentFileName(connection);
			}
			
			connection.disconnect();
			return len;
		} catch (IOException e) {
			Logger.getDefaultLogger().info("Http connect failed");
		}
		return -1;
	}
	
	private void getContentFileName(HttpURLConnection connection) throws UnsupportedEncodingException{
		String savePath = connection
				.getHeaderField("Content-Disposition");
		if (savePath != null
				&& savePath.contains("filename=")) {
			savePath = savePath.replaceFirst(
					".+filename=\"{0,1}(.+[^\"])\"{0,1}",
					"$1");
			savePath = new String(savePath.getBytes("ISO8859-1"),
					"utf-8");
		} else {
			savePath = null;
		}
		if (savePath == null) {
			savePath = urlString
					.substring(urlString.lastIndexOf("/") + 1);
		}
		if (savePathString == null) {
			super.savePathString = new File(savePath).getAbsolutePath();
		} else {
			savePathString = new File(savePathString + "/" + savePath)
					.getAbsolutePath();
		}
	}

	@Override
	InputStream openConnection(ConnectionContext context, long bytesBegin,
			long bytesEnd) throws IOException {
		HttpURLConnection connection = connect(bytesBegin);
		context.tokenObject = connection;
		return connection.getInputStream();
	}

	@Override
	protected void closeConnection(ConnectionContext context)
			throws IOException {
		super.closeConnection(context);
		if (context.tokenObject != null) {
			HttpURLConnection connection = (HttpURLConnection) context.tokenObject;
			connection.disconnect();
		}
	}

	@Override
	protected void saveDowanloadState(ObjectOutputStream outputStream)
			throws IOException {
		outputStream.writeUTF(urlString);
	}

	@Override
	protected void loadDowanloadState(ObjectInputStream inputStream)
			throws IOException {
		urlString = inputStream.readUTF();
	}

	@Override
	public String getProtocol() {
		return "http";
	}

	private static final Pattern HEADER_PATTERN = Pattern.compile("^(.+?)\\:\\s(.+?)$",
			Pattern.MULTILINE);
	
	static class HttpURLConnection {
		URL url;
		SocketChannel channel;
		ByteBuffer buffer = ByteBuffer.allocateDirect(64 * 1024);
		Map<String, String> headerMap = new TreeMap<String, String>();
		int responseCode;
		ReadStream readStream = null;

		HttpURLConnection(String urlString) throws MalformedURLException {
			url = new URL(urlString);
		}

		public InputStream getInputStream() {
			return readStream;
		}

		public long getContentLength() {
			return -1;
		}

		public String getHeaderField(String string) {
			return headerMap.get(string);
		}

		public int getResponseCode() {
			return responseCode;
		}

		public void disconnect() throws IOException {
			readStream.close();
			channel.close();
		}

		Pattern pattern = Pattern.compile("^(.+?)\\:\\s(.+?)$",
				Pattern.MULTILINE);
		
		public HttpURLConnection connect(long rangeBegin) throws IOException {
			
			channel = SocketChannel.open(new InetSocketAddress(url.getHost(),
					(url.getPort() == -1) ? 80 : url.getPort()));
			
			channel.configureBlocking(true);
			channel.socket().setSoTimeout(10000);
			
			String req = "GET " + url.getFile() + " HTTP/1.1\r\n";
			req += "Host: " + url.getHost() + "\r\n";
			req += "User-Agent: Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.101 Safari/537.36\r\n";
			req += "Connection: keep-alive\r\n";
			req += "Range: bytes=" + rangeBegin + "-\r\n";
			req += "\r\n";
			
			channel.write(ByteBuffer.wrap(req.getBytes()));
			
			ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
			int cnt = 0;
			byte[] headerEnd = new byte[] { '\r', '\n', '\r', '\n' };
			
			channel.read(buffer);
			buffer.flip();
			
			while (true) {
				if (buffer.hasRemaining()) {
					byte b = buffer.get();
					outputStream.write(b);
					if (b != headerEnd[cnt++]) {
						cnt = 0;
					} else if (cnt == 4) {
						break;
					}
				} else {
					buffer.position(0);
					if (channel.read(buffer) <= 0)
						throw new IOException("Invalid http response.");
					buffer.flip();
				}
			}
			
			String response = outputStream.toString();
			Matcher matcher = HEADER_PATTERN.matcher(response);
			while (matcher.find()) {
				headerMap.put(matcher.group(1), matcher.group(2));
			}
			
			int begin = response.indexOf(" ");
			int end = response.indexOf(" ", begin + 1);
			
			responseCode = Integer.valueOf(response.substring(begin + 1, end));
			if (responseCode / 100 == 3) {
				// 重定向
				String locString = getHeaderField("Location");
				if (!locString.contains("http://")) {

					locString = "http://" + url.getHost() + "/" + locString;
				}
				return new HttpURLConnection(locString).connect(rangeBegin);
			}
			
			readStream = new ReadStream(buffer.position(), buffer.limit());
			buffer.position(buffer.limit());
			buffer.limit(buffer.capacity());
			
			return this;
		}

		class ReadStream extends InputStream {

			int bufferBegin;
			int bufferEnd;

			public ReadStream(int bufferBegin, int bufferEnd) {
				super();
				this.bufferBegin = bufferBegin;
				this.bufferEnd = bufferEnd;
			}

			@Override
			public int read() throws IOException {
				throw new IOException("Do not use this method.");
			}

			@Override
			public int read(byte[] b) throws IOException {
				return read(b, 0, b.length);
			}

			@Override
			public int read(byte[] b, int off, int len) throws IOException {
				int c = channel.read(buffer);
				if (c > 0) {
					bufferEnd += c;
				}
				int remaining = bufferEnd - bufferBegin;
				if (len >= (remaining)) {
					buffer.position(bufferBegin);
					buffer.get(b, off, remaining);
					bufferBegin = 0;
					bufferEnd = 0;
					buffer.position(bufferBegin);
					buffer.limit(buffer.capacity());
					return remaining;
				} else {
					buffer.position(bufferBegin);
					buffer.get(b, off, len);
					bufferBegin += len;
					return len;
				}
			}

			@Override
			public long skip(long n) throws IOException {
				throw new IOException("Do not use this method.");
			}

			@Override
			public int available() throws IOException {
				throw new IOException("Do not use this method.");
			}

			@Override
			public void close() throws IOException {
				channel.shutdownInput();
			}

			@Override
			public synchronized void mark(int readlimit) {
				super.mark(readlimit);
			}

			@Override
			public synchronized void reset() throws IOException {
				throw new IOException("Do not use this method.");
			}

			@Override
			public boolean markSupported() {
				return super.markSupported();
			}

		}
	}
}
